﻿using System;
using System.Collections.Generic;
using System.Data.Objects;
using System.Data.Objects.DataClasses;
using System.Linq;
using System.Linq.Expressions;
using System.Text;
using Fintech.EntityFrameworkEngine;
using Fintech.Utils;
using Fintech.Utils.Diagnostics;
using ZOVNP.Model.Abstracts;

namespace ZOVNP.Engine
{
    /// <summary> Базовый класс для реализации потокобезопасного доступа к навигационным свойствам доменных объектов. </summary>
    internal abstract class RelationLocker : IDomainRelation
    {
        #region Классы

        /// <summary> Метаописание выражения, задающего способ получения значения ссылки (ключа). </summary>
        protected class ReferenceEntry
        {
            #region Поля

            /// <summary> Тип, на который ведёт ссылка. </summary>
            private Type destType;
            /// <summary> Хеш ссылочного выражения. Нужен для случая, когда одна сущность имеет несколько ссылок на другую сущность, но на разные её свойства. </summary>
            private int hash;
            /// <summary> До компиляции выражения это поле хранит само выражение, а после компиляции оно хранит уже делегат. </summary>
            private object exprOrDelegate;
            /// <summary> Блокировка для защиты от повторной компиляции выражения. </summary>
            private object compilationLock = new object();

            #endregion

            #region Конструкторы

            /// <summary> Создать описатель. </summary>
            /// <param name="DestinationType"> Тип, на который ведёт ссылка. </param>
            /// <param name="Expression"> Выражение для получения значения ссылки (при первом обращении будет откомпилировано). </param>
            /// <param name="ExpressionHash"> Хеш ссылочного выражения. Нужен для случая, когда одна сущность имеет несколько ссылок на другую сущность, но на разные её свойства. </param>
            public ReferenceEntry(Type DestinationType, LambdaExpression Expression, int ExpressionHash)
            {
                destType = DestinationType;
                exprOrDelegate = Expression;
                hash = ExpressionHash;
            }

            #endregion

            #region Свойства

            /// <summary> Тип, на который ведёт ссылка. </summary>
            public Type DestinationType { get { return destType; } }

            /// <summary> Хеш ссылочного выражения. Нужен для случая, когда одна сущность имеет несколько ссылок на другую сущность, но на разные её свойства. </summary>
            public int ExpressionHash { get { return hash; } }

            #endregion

            #region Методы

            /// <summary> Получение откомпилированного выражения. </summary>
            /// <returns> Делегат для получения значения ссылки (идентификатора). </returns>
            /// <remarks> Выражение компилируется только один раз, при первом обращении к нему. </remarks>
            public Delegate GetCompiledExpression()
            {
                var result = exprOrDelegate as Delegate;
                if (result == null) // оптимизация: если выражение уже откомпилировано, то не будет блокировки и входа в критическую секцию
                    lock (compilationLock) // TODO [Максимов Иван] В принципе, блокировку можно убрать, она здесь не критична.
                        exprOrDelegate = result = ((LambdaExpression)exprOrDelegate).Compile();
                return result;
            }

            /// <summary> Преобразовать к строке. </summary>
            /// <returns> Строковое представление объекта (для отладки). </returns>
            public override string ToString()
            {
                return string.Format("Ссылка на {0}, хеш выражения = {1}, {2}.", destType.Name, hash,
                    exprOrDelegate is Delegate ? "выражение уже откомпилировано в делегат" : "выражение ещё не компилировалось");
            }

            #endregion
        }

        #endregion

        #region Поля

        /// <summary> Навигационное свойство. </summary>
        protected RelatedEnd relation;
        /// <summary> Владелец исходящего конца навигационного свойства. </summary>
        private DomainObject owner;
        /// <summary> Блокировка на использование словаря метаданных. </summary>
        /// <remarks> Должна быть расположена именно в этом (базовом) классе, а не в шаблонном <c>ReferenceLocker</c>, чтобы существовать в единственном экземпляре. </remarks>
        protected static object referencesLock = new object();
        /// <summary> Словарь метаданных ссылочных выражений. </summary>
        /// <remarks> Должен быть расположен именно в этом (базовом) классе, а не в шаблонном <c>ReferenceLocker</c>, чтобы существовать в единственном экземпляре. </remarks>
        protected static Dictionary<Type, List<ReferenceEntry>> references = new Dictionary<Type, List<ReferenceEntry>>();

        #endregion

        #region Конструкторы

        /// <summary> конструктор со связью </summary>
        /// <param name="Owner"> Доменный объект - собственник навигационного свойства (исходящий конец связи). </param>
        /// <param name="Relation"> связь </param>
        protected RelationLocker(DomainObject Owner, RelatedEnd Relation)
        {
            Owner.CheckOnNull("Owner");
            Relation.CheckOnNull("Relation");
            relation = Relation;
            owner = Owner;
        }

        #endregion

        #region События

        /// <summary> Событие по добавлению/удалению элементов коллекции. </summary>
        //event System.ComponentModel.CollectionChangeEventHandler IDomainRelation.AssociationChanged
        //{
        //    add { relation.AssociationChanged += value; ; }
        //    remove { relation.AssociationChanged -= value; ; }
        //}

        #endregion

        #region Свойства

        /// <summary> Получить признак того, что навигационное свойство уже было загружено из базы данных. </summary>
        public bool IsLoaded { get { return relation.IsLoaded; } }

        /// <summary> Загрузить навигационное свойство. </summary>
        /// <remarks> Загрузка навигационных свойств не поддерживается в состоянии <c>Added</c>, вызов метод не даст эффекта. </remarks>
        public void Load() { TryLoadNavigation(false); }

        /// <summary> Получить владельца навигационного свойства. </summary>
        public DomainObject Owner { get { return owner; } }

        /// <summary> Получить значение навигационного свойства. </summary>
        /// <remarks> 
        /// Значение навигационного свойства зависит от типа навигации (1 к многим, многие к 1, 1 к 1 и тд) и может возвращать либо ссылку
        /// на единичный объект, либо ссылку на коллекцию объектов. В случае если возвращается ссылка на объект, то она может иметь значение null.
        /// Если же возвращается ссылка на коллекцию объектов, и эта коллекция пуста, то свойство содержит экземпляр пустой коллекции. 
        /// </remarks>
        public abstract object Navigation { get; }

        #endregion

        #region Методы

        /// <summary> Обработчик для окончания загрузки навигационного свойства. </summary>
        protected virtual void OnRelationLoaded() { }

        /// <summary> Внутренний метод загрузки навигационного свойства. </summary>
        /// <param name="UseLocalIfExists"> True, если нет необходимости загружать навигационное свойство заново, если оно уже загружено. </param>
        protected void TryLoadNavigation(bool UseLocalIfExists)
        {
            if (Owner.EntityState == System.Data.EntityState.Added) return; // Вызов Load() при таком состоянии приведет к ошибке
            while (true)
            {
                if (UseLocalIfExists)
                    UseLocalIfExists = relation.IsLoaded;
                if (UseLocalIfExists) break;
                if (Owner.EntityState == System.Data.EntityState.Detached)
                    throw new InvalidOperationException(string.Format(Properties.Resources.MSG_NavigationPropertiesForbidden, Owner.EntityState));
                var engine = Owner.Context.Engine;
                if (engine == null) throw new InvalidOperationException(Properties.Resources.MSG_EngineIsNull);
                bool entered = false; // признак удачного захвата блокировки

                DebugTuner.Instance.Logger.WriteLine(() => engine.ProviderAdapter.ToTraceString((ObjectQuery)((IRelatedEnd)relation).CreateSourceQuery()));
                try
                {
                    engine.EnterDatabaseCriticalSection(out entered);
                    relation.Load(System.Data.Objects.MergeOption.OverwriteChanges);
                    OnRelationLoaded();
                    break;
                }
                catch (NestedQueryInTheSameThreadException) { entered = false; throw; }
                catch (System.Threading.ThreadAbortException) { } // ошибку от Thread.Abort() нужно пропускать наверх
                catch (Exception Ex)
                {
                    if (!engine.HandleException(Ex, Owner.Context))
                        throw;
                }
                finally { if (entered) engine.ExitDatabaseCriticalSection(); }
            }
        }

        #endregion
    }
}
